/*
 * Copyright:  2018 smarabbit studio.
 *
 *  Licensed under the Confluent Community License; you may not use this file
 *  except in compliance with the License.  You may obtain a copy of the License at
 *
 *  http://www.confluent.io/confluent-community-license
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 *  WARRANTIES OF ANY KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations under the License.
 *
 *  @作   者： 黄开晖 (117227773@qq.com)
 *  @日   期:  2021年8月7日
 */
package com.massyframework.beanskin.runtime.framework.installing.fragment;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarFile;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.massyframework.beanskin.ModuleIdentifier;
import com.massyframework.beanskin.ModuleJarsLocator;
import com.massyframework.beanskin.ModuleRepository;
import com.massyframework.beanskin.StartStopAbility;
import com.massyframework.beanskin.assembly.Assembly;
import com.massyframework.beanskin.fragment.Fragment;
import com.massyframework.beanskin.fragment.FragmentBuilder;
import com.massyframework.beanskin.framework.Framework;
import com.massyframework.beanskin.runtime.ModuleRegistry;
import com.massyframework.beanskin.runtime.framework.ExceptionCollectExecutorService;
import com.massyframework.beanskin.service.ServiceRepositoryFactory;
import com.massyframework.beanskin.util.IOUtils;
import com.massyframework.beanskin.util.localstore.LocalStore;

/**
 * 片段监视器
 * @author huangkh
 *
 */
public class FragmentMonitor implements StartStopAbility{
	
	private static final String MODULE = "MODULE";
	
	private Logger logger = LoggerFactory.getLogger(FragmentMonitor.class);
	
	private Framework framework;
	private Assembly kernel;
	
	private ModuleRepository moduleRepository;
	private ModuleJarsLocator moduleJarsLocator;
	private ServiceRepositoryFactory serviceRepositoryFactory;
	private LocalStore localStore;
	
	private Thread thread;
	
	private Map<String, Entry> fileMap;

	/**
	 * 构造方法
	 * @param builder
	 */
	protected FragmentMonitor(Builder builder) {
		this.framework = Objects.requireNonNull(builder.framework, "\"framework\" cannot be null.");
		this.kernel = Objects.requireNonNull(builder.kernel, "\"kernel\" cannot be null.");
		this.moduleRepository = Objects.requireNonNull(builder.moduleRepository, "\"moduleRepository\" cannot be null.");
		this.moduleJarsLocator = Objects.requireNonNull(builder.moduleJarsLocator, "\"moduleJarsLocator\" cannot be null.");
		this.serviceRepositoryFactory = Objects.requireNonNull(builder.serviceRepositoryFactory, "\"serviceRepositoryFactory\" cannot be null.");
		
		this.localStore = Objects.requireNonNull(builder.localStore, "\"localStore\" cannot be null.");
		
		this.fileMap = new ConcurrentHashMap<>();
	}
	
	@Override
	public void start() throws Exception {
		if (this.thread == null) {
			Path dir = this.localStore.getFragmentDirectory();
			ModuleJarFileWatcher watcher = 
					new ModuleJarFileWatcher(dir, this::onEvent);
			this.thread = new Thread(watcher);
			this.thread.start();
						
			FragmentInstaller installer = new FragmentInstaller();
			Map<ModuleIdentifier, FragmentBuilder> builderMap =
					installer.loadingFragmentBuilders(this.getKernel(), this.getLocalStore());
			
			//扫描并注册插件
			ExecutorService taskExecutor = new ExceptionCollectExecutorService(Executors.newFixedThreadPool(4));	
			installer.installFragments(new ArrayList<>(builderMap.values()), 
					false,
					this.getModuleJarsLocator(), 
					this.getModuleRepository(), 
					this.getServiceRepositoryFactory(), 
					this.getFramework(), 
					this.getKernel(), 
					taskExecutor);
			taskExecutor.shutdown();
			try {
			  taskExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
			} catch (InterruptedException e) {

			}
				
			Map<Path, FragmentJarFile> map = new HashMap<>();
			Files.list(dir).forEach( path-> {
				FragmentJarFile file = this.resolvePath(path);
				if (file != null) {
					map.put(path, file);
				}
			});
						
			for (Map.Entry<Path, FragmentJarFile> entry: map.entrySet()) {
				Path path = entry.getKey();
				String filePath = path.toFile().getPath();
				
				FragmentJarFile jarFile = entry.getValue();				
				Entry fileEntry = this.fileMap.computeIfAbsent(filePath, key->{
					try {
						return new Entry(DigestUtils.md5Hex(new FileInputStream(path.toFile())));
					}catch(IOException e) {
						if (logger.isErrorEnabled()) {
							logger.error(e.getMessage(), e);
						}
						return null;
					}
				});
				
				if (fileEntry != null) {
					Set<ModuleIdentifier> mis = jarFile.getModuleIdentifiers();
					
					for (ModuleIdentifier mi: mis) {
						Fragment fragment = this.getModuleRepository()
								.findModuleByIdentifier(mi, Fragment.class);
						if (fragment != null) {
							fileEntry.getModuleIdentifiers().add(mi);
						}else {
							//部署
							Path deployPath = this.deployment(mi, jarFile);
							//安装
							//创建并安装片段
							try {
								FragmentBuilder builder = 
										installer.loadingFragmentBuilder(this.getKernel(),this.getLocalStore(), mi, deployPath);
								installer.createAndRegistFragment(builder, 
										true,
										this.getModuleJarsLocator(), 
										this.getModuleRepository(), 
										this.getServiceRepositoryFactory(), 
										this.getFramework(),
										this.getKernel());
								fileEntry.getModuleIdentifiers().add(mi);
							}catch(Exception e) {
								if (logger.isErrorEnabled()) {
									logger.error("install fragment failed.", e);
								}
							}
						}
					}
				}
			}
		}
	}
	
	protected FragmentJarFile resolvePath(Path path) {
		File file = path.toFile();
		if (file.isDirectory()) return null;
		
		if (file.getName().endsWith(".jar")) {
			try {
				JarFile jarFile = new JarFile(file);
				
				if (jarFile.getJarEntry(MODULE) == null) {
					//非模块包，则跳过.
					IOUtils.close(jarFile);
					return null;
				}
				
				return new FragmentJarFile(jarFile);
			}catch(Exception e) {
				if (logger.isErrorEnabled()) {
					logger.error(e.getMessage(), e);
				}
			}
		}
		return null;
	}

	@Override
	public void stop() throws Exception {
		if (this.thread != null) {
			this.thread.interrupt();
			this.thread = null;
		}
	}

	/**
	 * 事件处理
	 * @param event
	 */
	protected void onEvent(WatchEvent<?> event) {		 
		 if (event.context() != null) {
			 if (event.context() instanceof Path) {
				 Path path = (Path)event.context();
				 path = this.getLocalStore().getFragmentDirectory().resolve(path);
				 
				 if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
					 this.addFile(path);
					 return;
				 }
				 
				 if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
					 this.deleteFile(path);
					 return;
				 }
				 
				 if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
					 String filePath = path.toFile().getPath();
					 try {
						 String digest = DigestUtils.md5Hex(new FileInputStream(path.toFile()));
						 
						 Entry entry = this.fileMap.get(filePath);
						 if (entry == null) return;
						
						 //摘要相同，则表示文件未改变，直接返回
						if (entry.getDigest().equals(digest)) {
							return;
						}
					 }catch(IOException e) {
						 if (logger.isErrorEnabled()) {
							 logger.error(e.getMessage(), e);
						 }
						 return;
					 }
					 
					 
					 this.modifyFile(path);
					 return;
				 }
			 }
		 }
	}
	
	/**
	 * 添加文件
	 * @param path {@link Path},文件路经
	 */
	protected void addFile(Path path) {
		FragmentJarFile jarFile = this.resolvePath(path);
		FragmentInstaller installer = new FragmentInstaller();
		if (jarFile != null) {
			Set<ModuleIdentifier> mis = jarFile.getModuleIdentifiers();
			for (ModuleIdentifier mi: mis) {
				Fragment fragment = this.getModuleRepository().findModuleByIdentifier(mi, Fragment.class);
				if (fragment == null) {
					//解包，部署
					Path deployPath = this.deployment(mi, jarFile);
					Entry entry = 
						this.fileMap.computeIfAbsent(path.toFile().getPath(),
								key ->{
									try {
										return new Entry(DigestUtils.md5Hex(new FileInputStream(path.toFile())));
									}catch(IOException e) {
										if (logger.isErrorEnabled()) {
											logger.error(e.getMessage(), e);
										}
										return null;
									}
								});
					if (entry != null) {
						entry.getModuleIdentifiers().add(mi);
						
						//创建并安装片段
						try {
							FragmentBuilder builder = 
									installer.loadingFragmentBuilder(this.getKernel(), this.getLocalStore(), mi, deployPath);
							installer.createAndRegistFragment(builder, 
									true,
									this.getModuleJarsLocator(), 
									this.getModuleRepository(), 
									this.getServiceRepositoryFactory(), 
									this.getFramework(),
									this.getKernel());
						}catch(Exception e) {
							if (logger.isErrorEnabled()) {
								logger.error("install fragment failed.", e);
							}
						}
					}
				}
			}
			
			IOUtils.close(jarFile.getJarFile());
		}
	}
	
	protected void modifyFile(Path path) {
		String filePath = path.toFile().getPath();		
		final FragmentJarFile jarFile = this.resolvePath(path);
		final ModuleRegistry registry = (ModuleRegistry)this.getModuleRepository();
		final FragmentInstaller installer = new FragmentInstaller();
		
		Entry entry = this.fileMap.remove(filePath);
		Set<ModuleIdentifier> newIds = jarFile.getModuleIdentifiers();
		
		if (entry != null) {
			for (ModuleIdentifier mi: entry.getModuleIdentifiers()) {
				Fragment fragment = this.getModuleRepository().findModuleByIdentifier(mi, Fragment.class);
				if (fragment != null) {
					if (newIds.remove(mi)) {
						//存在就替换
						try {
							registry.reloadFragment(mi, builder-> {
								return this.replaceFragment(mi, builder, path, jarFile, installer);
							});
						}catch(Exception e) {
							if (logger.isErrorEnabled()) {
								logger.error(e.getMessage(), e);
							}
						}
					}else {
						//不存在则卸载
						this.getModuleRepository().uninstall(fragment);
						//取消发布
						this.undeployment(mi);
					}
				}
			}
		}
		
		if (!newIds.isEmpty()) {
			for (ModuleIdentifier mi: newIds) {
				Fragment fragment = this.getModuleRepository().findModuleByIdentifier(mi, Fragment.class);
				if (fragment == null) {
					//解包，部署
					Path deployPath = this.deployment(mi, jarFile);
					
					Entry newEntry = 
							this.fileMap.computeIfAbsent(path.toFile().getPath(),
									key ->{ 
										try {
											return new Entry(DigestUtils.md5Hex(new FileInputStream(path.toFile())));
										}catch(IOException e) {
											if (logger.isErrorEnabled()) {
												logger.error(e.getMessage(), e);
											}
											return null;
										}
									});
					if (newEntry != null) {
						newEntry.getModuleIdentifiers().add(mi);
						
						//创建并安装片段
						try {
							FragmentBuilder builder = 
									installer.loadingFragmentBuilder(this.getKernel(), this.getLocalStore(), mi, deployPath);
							installer.createAndRegistFragment(builder, 
									true,
									this.getModuleJarsLocator(), 
									this.getModuleRepository(), 
									this.getServiceRepositoryFactory(), 
									this.getFramework(),
									this.getKernel());
						}catch(Exception e) {
							if (logger.isErrorEnabled()) {
								logger.error("install fragment failed.", e);
							}
						}
					}
				}
			}
		}
		
		IOUtils.close(jarFile.getJarFile());
	}
	
	protected void deleteFile(Path path) {
		String filePath = path.toFile().getPath();
		Entry entry = this.fileMap.remove(filePath);
		if (entry != null) {
			for (ModuleIdentifier mi: entry.getModuleIdentifiers()) {
				Fragment fragment = this.getModuleRepository().findModuleByIdentifier(mi, Fragment.class);
				if (fragment != null) {
					//卸载
					this.getModuleRepository().uninstall(fragment);
				}
				
				//取消部署
				this.undeployment(mi);
			}
		}
	}
	
	/**
	 * 替换片段
	 * @param identifier {@link ModuleIdentifier},模块标识符
	 * @param builder {@link FragmentBuilder},模块构建器
	 * @param path  {@link Path}, 修改的文件
	 * @param jarFIle {@link FragmentJarFile}, 片段jar文件
	 * @param installer {@link FragmentInstaller},片段安装器
	 * @param jarFile
	 */
	protected Fragment replaceFragment(ModuleIdentifier identifier, FragmentBuilder builder,
			Path path, FragmentJarFile jarFile, FragmentInstaller installer) {
		//先取消旧片段的部署
		this.undeployment(identifier);
		
		//在部署新的片段
		Path deployPath = this.deployment(identifier, jarFile);
		Entry entry = 
				this.fileMap.computeIfAbsent(path.toFile().getPath(),
						key ->{ 
							try {
								return new Entry(DigestUtils.md5Hex(new FileInputStream(path.toFile())));
							}catch(IOException e) {
								if (logger.isErrorEnabled()) {
									logger.error(e.getMessage(), e);
								}
								return null;
							}
						});
		
		if (entry != null) {
			entry.getModuleIdentifiers().add(identifier);
			
			//创建片段
			try {
				installer.loadingFragmentXml(this.getKernel(), this.getLocalStore(), identifier, deployPath, builder);
				return installer.createFragment(builder, 
						this.getModuleJarsLocator(), 
						this.getModuleRepository(), 
						this.getServiceRepositoryFactory(), 
						this.getFramework(),
						this.getKernel());
			}catch(Exception e) {
				if (logger.isErrorEnabled()) {
					logger.error("install fragment failed.", e);
				}
			}
		}
		
		return null;
	}
	
	/**
	 * 部署
	 * @param mi {@link ModuleIdentifier}
	 * @param jarFile {@link FragmentJarFile}
	 * @return {@link Path},部署路经
	 */
	protected Path deployment(ModuleIdentifier mi, FragmentJarFile jarFile) {
		Path path = this.getLocalStore().getDeploymentDirectory(mi);
		if (Files.exists(path)) {
			File file = path.toFile();
			this.deleteDirectory(file);
		}
				
		String name = MODULE.concat(FragmentJarFile.SEPARATOR).concat(mi.getGroupId())
					.concat(FragmentJarFile.SEPARATOR).concat(mi.getModuleId())
					.concat(FragmentJarFile.SEPARATOR).concat(mi.getVersion().toString());
		int length = name.length() + 1;
		this.doCopyFile(name, length, path, jarFile);		
		return path;
	}
	
	protected void doCopyFile(String name, int length, Path path,  FragmentJarFile jarFile) {
		List<String> chlidNames = jarFile.getChildrens(name);
		if (!chlidNames.isEmpty()) {
			for (String childName: chlidNames) {
				if (jarFile.isDirectory(childName)) {
					doCopyFile(childName, length, path, jarFile);
				}else {
					String fileName = StringUtils.substring(childName, length);
					
					InputStream is = null;
					try {
						is = jarFile.getInputStream(childName);
						if (is != null) {
							Path target = path.resolve(fileName);
							Files.createDirectories(target);
							Files.copy(is, target,  StandardCopyOption.REPLACE_EXISTING);
						}
					}catch(Exception e) {
						if (logger.isErrorEnabled()) {
							logger.error(e.getMessage(), e);
						}
					}finally {
						IOUtils.close(is);
					}
				}
				
			}
		}
	}
	
	/**
	 * 取消模块部署
	 * @param mi {@link ModuleIdentifier}
	 */
	protected void undeployment(ModuleIdentifier mi) {
		Path path = this.getLocalStore().getDeploymentDirectory(mi);
		File file = path.toFile();
		this.deleteDirectory(file);
		
		//判断是否需要删除moduleId目录
		File moduleIdDir = file.getParentFile();
		File[] childFiles = moduleIdDir.listFiles();
		if (childFiles == null || childFiles.length == 0) {
			moduleIdDir.delete();
		}
		
		//判断是否需要删除groupId目录
		File groupIdDir = moduleIdDir.getParentFile();
		childFiles = groupIdDir.listFiles();
		if (childFiles == null || childFiles.length == 0) {
			groupIdDir.delete();
		}
	}
	
	/**
	 * 递归删除目录
	 * @param directory {@link File},目录
	 */
	protected void deleteDirectory(File directory) {
		if (!directory.exists()) return;
		
		File[] files = directory.listFiles();
		if (files != null) {
			for (File file: files) {
				if (file.isDirectory()) {
					this.deleteDirectory(file);
				}else {
					file.delete();
				}
			}
		}
		
		directory.delete();
	}

	/**
	 * 运行框架
	 * @return {@link Framework}
	 */
	public Framework getFramework() {
		return this.framework;
	}
	
	/**
	 * 内核装配件
	 * @return {@link Assembly}
	 */
	public Assembly getKernel() {
		return this.kernel;
	}
	
	/**
	 * 模块资源库
	 * @return {@link ModuleRepository}
	 */
	public ModuleRepository getModuleRepository() {
		return this.moduleRepository;
	}
	
	/**
	 * 模块jar包定位器
	 * @return {@link ModuleJarsLocator}
	 */
	public ModuleJarsLocator getModuleJarsLocator() {
		return this.moduleJarsLocator;
	}
	
	/**
	 * 服务资源库工厂
	 * @return {@link ServiceRepositoryFactory}
	 */
	public ServiceRepositoryFactory getServiceRepositoryFactory() {
		return this.serviceRepositoryFactory;
	}
	
	/**
	 * 本地存储
	 * @return {@link LocalStore}
	 */
	public LocalStore getLocalStore() {
		return this.localStore;
	}

	/**
	 * 创建构建器实例
	 * @return {@link Builder}
	 */
	public static Builder newBuilder() {
		return new Builder();
	}
	
	public static class Builder {
		private Framework framework;
		private Assembly kernel;
		
		private ModuleRepository moduleRepository;
		private ModuleJarsLocator moduleJarsLocator;
		private ServiceRepositoryFactory serviceRepositoryFactory;
		private LocalStore localStore;
		
		/**
		 * 设置运行框架
		 * @param value {@link Framework}
		 * @return {@link Builder}
		 */
		public Builder framework(Framework value) {
			this.framework = value;
			return this;
		}
		
		/**
		 * 设置内核装配件
		 * @param value {@link Assembly}
		 * @return {@link Builder}
		 */
		public Builder kernel(Assembly value) {
			this.kernel = value;
			return this;
		}
		
		/**
		 * 设置模块jar包定位器
		 * @param value {@link ModuleJarsLocator}
		 * @return
		 */
		public Builder moduleJarsLocator(ModuleJarsLocator value) {
			this.moduleJarsLocator = value;
			return this;
		}
		
		/**
		 * 设置模块资源库
		 * @param value {@link ModuleRepository}
		 * @return {@link Builder}
		 */
		public Builder moduleRepository(ModuleRepository value) {
			this.moduleRepository = value;
			return this;
		}
		
		/**
		 * 设置服务资源库工厂
		 * @param value {@link ServiceRepositoryFactory}
		 * @return {@link Builder}
		 */
		public Builder serviceRepositoryFactory(ServiceRepositoryFactory value) {
			this.serviceRepositoryFactory = value;
			return this;
		}
		
		/**
		 * 设置本地存储
		 * @param value {@link LocalStore}
		 * @return
		 */
		public Builder localStore(LocalStore value) {
			this.localStore = value;
			return this;
		}
		
		public FragmentMonitor build() {
			return new FragmentMonitor(this);
		}
	}
	
	/**
	 * 记录
	 * @author huangkh
	 *
	 */
	private class Entry {
		private String digest;
		private Set<ModuleIdentifier> ids;
		
		public Entry(String digest) {
			this.digest = Objects.requireNonNull(digest, "\"digest\" cannot be null.");
			this.ids = new CopyOnWriteArraySet<>();
		}
		
		/**
		 * 摘要
		 * @return
		 */
		public String getDigest() {
			return this.digest;
		}
		
		/**
		 * 模块标识符
		 * @return {@link Set}
		 */
		public Set<ModuleIdentifier> getModuleIdentifiers(){
			return this.ids;
		}
	}
}
